﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using DotNetRepository.Tools;

namespace DotNetRepository
{
    public static class EntityReflector<TEntity> where TEntity : class, IEntity
    {
        private static readonly IEnumerable<MemberInfo> EntityRefs;
        private static readonly IEnumerable<MemberInfo> EntityEnumerations;
        private static readonly IEnumerable<MemberInfo> EntityCollections;
        private static readonly IEnumerable<MemberInfo> ChildEntitiesToPersist;
        private static readonly IEnumerable<MemberInfo> ChildEntitiesToDelete;
        

        public static readonly Dictionary<MemberInfo, MemberInfo> EntityRefEnumerationPairs = new Dictionary<MemberInfo, MemberInfo>();

        public readonly static bool HasEntityCollections;
        public readonly static bool HasChildEntitiesToPersist;
        public readonly static bool HasChildEntitiesToDelete;
        public readonly static bool HasEntityRefs;


        private static readonly Dictionary<Type, Dictionary<Type, object>> GenericRepositories = new Dictionary<Type, Dictionary<Type, object>>();
        private static object GetGenericRepository(Type targetType, IEntityRepository<TEntity> repository)
        {
            lock (GenericRepositories)
            {
                Dictionary<Type, object> repositoryDictionary;
                if (GenericRepositories.ContainsKey(repository.GetType()))
                {
                    repositoryDictionary = GenericRepositories[repository.GetType()];
                    if (repositoryDictionary.ContainsKey(targetType))
                    {
                        return repositoryDictionary[targetType];
                    }
                }
                else
                {
                    repositoryDictionary = new Dictionary<Type, object>();
                    GenericRepositories.Add(repository.GetType(), repositoryDictionary);  
                }

                var targetRepository =
                    repository.GetType().GetMethod("GetGenericRepository").MakeGenericMethod(new[] { targetType })
                    .Invoke(repository, null);

                repositoryDictionary.Add(targetType, targetRepository);
            
                return targetRepository;
            }
        }

        static EntityReflector()
        {
            EntityRefs = typeof(TEntity).MembersInheritingFrom<IEntityRef>();
            HasEntityRefs = (EntityRefs != null && EntityRefs.Count() > 0);

            EntityEnumerations = typeof(TEntity).MembersInheritingFrom<IEntityEnumeration>();
            EntityCollections = 
                typeof(TEntity).MembersInheritingFrom<IEntityCollection>()
                .Union(
                    typeof(TEntity).GetAllInstanceMembers()
                    .Where(m => m.GetCustomAttributes(typeof(EntityCollectionAttribute), true).Count() > 0))
                .Distinct();

            HasEntityCollections = EntityCollections.Count() > 0;

            // ReflectPersistanceOptions
            var memberInfoSet =
                from memberInfo in typeof(TEntity).GetAllInstanceMembers()
                where memberInfo.GetCustomAttributes(typeof(PersistanceOptionsAttribute), true).Count() > 0 &&
                    (memberInfo.MemberType == MemberTypes.Field || memberInfo.MemberType == MemberTypes.Property)
                select memberInfo;

            ChildEntitiesToPersist =
                memberInfoSet.Where(
                    memberInfo =>
                    (
                        ((PersistanceOptionsAttribute) memberInfo.GetCustomAttributes(typeof(PersistanceOptionsAttribute), true).FirstOrDefault()).SaveWithEntityNotNull() &&
                        memberInfo.GetReturnType().GetInterfaces().Contains(typeof (IEntity))
                    ));

            HasChildEntitiesToPersist = ChildEntitiesToPersist.Count() > 0;

            ChildEntitiesToDelete = from memberInfo in memberInfoSet
                                    where ((PersistanceOptionsAttribute)memberInfo.GetCustomAttributes(typeof(PersistanceOptionsAttribute), true).FirstOrDefault()).DeleteWithEntityNotNull() &&
                                    memberInfo.GetReturnType().GetInterfaces().Contains(typeof(IEntity))
                                    select memberInfo;

            HasChildEntitiesToDelete = ChildEntitiesToDelete.Count() > 0;


            // Reflect Enumeration Pairs
            foreach (var entityRef in EntityRefs)
            {
                var attribute = entityRef.GetCustomAttributes(typeof(CorrespondingEnumerationAttribute), false).FirstOrDefault() as CorrespondingEnumerationAttribute;
                var enumerationName = attribute != null ? attribute.Name : null;
                if (enumerationName == null) continue;  // doesn't have a corresponding enumeration

                var enumerationContainerType = entityRef.GetReturnType().GetGenericArguments().FirstOrDefault();
                if (enumerationContainerType == null)
                {
                    throw new ApplicationException("Error binding EntityRef with EntityCollection!");
                }

                var enumeration = enumerationContainerType.GetField(
                    enumerationName, BindingFlags.NonPublic | BindingFlags.Instance);

                if (enumeration == null)
                    throw new ApplicationException(
                        string.Format("Error binding EntityRef with EntityCollection!  '{0}' not found in class '{1}'.",
                        enumerationName, enumerationContainerType.Name));

                if (!enumeration.FieldType.GetInterfaces().Contains(typeof(IEntityEnumeration))
                    || enumeration.FieldType.GetGenericArguments().FirstOrDefault() != typeof(TEntity))
                    throw new ApplicationException(
                        String.Format("Error binding EntityRef with EntityEnumeration!  '{0}.{1}' is not a valid EntityEnumeration<{2}>!",
                        enumerationContainerType, enumerationName, typeof(TEntity).Name));

                EntityRefEnumerationPairs.Add(entityRef, enumeration);
            }
        }

        public static IEnumerable<IEntityRef> GetEntityRefs(TEntity entity)
        {
            var instanceEntityRefs = new HashSet<IEntityRef>();
            foreach (var entityRef in EntityRefs.GetFields())
            {
                instanceEntityRefs.Add(entityRef.GetValue(entity) as IEntityRef);
            }
            foreach (var entityRef in EntityRefs.GetProperties())
            {
                instanceEntityRefs.Add(entityRef.GetValue(entity, null) as IEntityRef);
            }
            return instanceEntityRefs;
        }

        public static IEnumerable<IEntityEnumeration> GetEntityEnumerations(TEntity entity)
        {
            var instanceEntityEnumerations = new HashSet<IEntityEnumeration>();
            foreach (var entityEnumeration in EntityEnumerations.GetFields())
                instanceEntityEnumerations.Add(entityEnumeration.GetValue(entity) as IEntityEnumeration);
            foreach (var entityEnumeration in EntityEnumerations.GetProperties())
                instanceEntityEnumerations.Add(entityEnumeration.GetValue(entity, null) as IEntityEnumeration);
            return instanceEntityEnumerations;
        }

        public static void InitializeEntityCollections(TEntity entity, IEntityRepository<TEntity> repository)
        {
            GetEntityCollections(entity, repository);
        }

        public static IEntityCollection<T> GetEntityCollection<T>(ICollection<T> collection, TEntity entity, MemberInfo memberInfo, IEntityRepository<TEntity> repository)
            where T : class, IEntity
        {
            var entityCollection = collection as IEntityCollection<T>;
            if (entityCollection == null)
            {
                var attribute = memberInfo.GetCustomAttributes(typeof(EntityCollectionAttribute), true).Single() as EntityCollectionAttribute;

                var targetType = typeof(T);

                var entityCollectionType = typeof(EntityCollection<>).MakeGenericType(targetType);
                var entityCollectionArgsType = typeof(EntityCollectionArgs<>).MakeGenericType(targetType);

                if (String.IsNullOrEmpty(attribute.ForeignKey))
                {
                    attribute.ForeignKey = typeof(TEntity).Name + "ID";
                }

                var entityCollectionArgs = entityCollectionArgsType.GetConstructor(Type.EmptyTypes).Invoke(null);
                entityCollectionArgsType.SetValue("DeleteOnRemove", entityCollectionArgs, attribute.DeleteOnRemove);
                entityCollectionArgsType.SetValue("ForeignKey", entityCollectionArgs, attribute.ForeignKey);

                var constructor = entityCollectionType.GetConstructor(new[] { entityCollectionArgsType });

                entityCollection = constructor.Invoke(new[] { entityCollectionArgs }) as IEntityCollection<T>;

                foreach (var obj in collection)
                {
                    entityCollection.Add(obj);
                }

                entity.GetType().SetValue(memberInfo.Name, entity, entityCollection);
            }
            
            if (!entityCollection.Initialized)
            {
                entityCollection.Initialize(entity);
            }

            return entityCollection;
        }

        public static IEnumerable<IEntityCollection> GetEntityCollections(TEntity entity, IEntityRepository<TEntity> repository)
        {
            var instanceEntityCollections = new List<IEntityCollection>(EntityCollections.Count());
            instanceEntityCollections.AddRange(EntityCollections.Select(
                memberInfo =>
                    {
                        var collection = memberInfo.GetValue(entity);
                        var targetType = collection.GetType().GetGenericArguments().First();

                        return 
                            typeof(EntityReflector<TEntity>)
                            .GetMethod("GetEntityCollection")
                            .MakeGenericMethod(new[] {targetType})
                            .Invoke(null, new[] {collection, entity, memberInfo, repository})
                            as IEntityCollection;
                    }));

            return instanceEntityCollections;
        }

        public static IEnumerable<IEntity> GetChildEntitiesToPersist(TEntity entity)
        {
            var entities = new List<IEntity>(ChildEntitiesToPersist.Count());
            entities.AddRange(ChildEntitiesToPersist.GetFields().Select(fieldInfo => fieldInfo.GetValue(entity)).OfType<IEntity>());

            entities.AddRange(ChildEntitiesToPersist.GetProperties().Select(propInfo => propInfo.GetValue(entity, null)).OfType<IEntity>());

            return entities;
        }

        public static IEnumerable<IEntity> GetChildEntitiesToDelete(TEntity entity)
        {
            var entities = new List<IEntity>(ChildEntitiesToDelete.Count());
            entities.AddRange(ChildEntitiesToDelete.GetFields().Select(fieldInfo => fieldInfo.GetValue(entity)).OfType<IEntity>());

            entities.AddRange(ChildEntitiesToDelete.GetProperties().Select(propInfo => propInfo.GetValue(entity, null)).OfType<IEntity>());

            return entities;
        }

    }
}
